﻿// VexFlow - Music Engraving for HTML5
// Copyright Mohit Muthanna 2010
//
// This class implements various types of modifiers to notes (e.g. bends,
// fingering positions etc.) Accidentals should also be implemented as
// modifiers, eventually.

/**
 * @constructor
 */
Vex.Flow.ModifierContext = function () {
    // Current modifiers
    this.modifiers = {};

    // Formatting data.
    this.preFormatted = false;
    this.width = 0;
    this.spacing = 0;
    this.state = {
        left_shift: 0,
        right_shift: 0,
        text_line: 0
    };
}

Vex.Flow.ModifierContext.prototype.addModifier = function (modifier) {
    var type = modifier.getCategory();
    if (!this.modifiers[type]) this.modifiers[type] = [];
    this.modifiers[type].push(modifier);
    modifier.setModifierContext(this);
    this.preFormatted = false;
    return this;
}

Vex.Flow.ModifierContext.prototype.getModifiers = function (type) {
    return this.modifiers[type];
}

Vex.Flow.ModifierContext.prototype.getWidth = function () {
    return this.width;
}

Vex.Flow.ModifierContext.prototype.getExtraLeftPx = function () {
    return this.state.left_shift;
}

Vex.Flow.ModifierContext.prototype.getExtraRightPx = function () {
    return this.state.right_shift;
}

Vex.Flow.ModifierContext.prototype.getMetrics = function (modifier) {
    if (!this.formatted) throw new Vex.RERR("UnformattedModifier",
        "Unformatted modifier has no metrics.");

    return {
        width: this.state.left_shift + this.state.right_shift + this.spacing,
        spacing: this.spacing,
        extra_left_px: this.state.left_shift,
        extra_right_px: this.state.right_shift
    }
}

Vex.Flow.ModifierContext.prototype.formatNotes = function () {
    var notes = this.modifiers['stavenotes'];
    if (!notes || notes.length < 2) return this;

    if (notes[0].getStave() != null)
        return this.formatNotesByY(notes);

    // Assumption: only two notes
    Vex.Assert(notes.length == 2,
        "Got more than two notes in Vex.Flow.ModifierContext.formatNotes!");

    var top_note = notes[0];
    var bottom_note = notes[1];

    // If notes intersect, then shift the bottom stemmed note right
    if (notes[0].getStemDirection() == Vex.Flow.StaveNote.STEM_DOWN) {
        bottom_note = notes[0];
        top_note = notes[1];
    }

    var top_keys = top_note.getKeyProps();
    var bottom_keys = bottom_note.getKeyProps();

    // XXX: Do this right (by key, not whole note).
    var x_shift = 0;
    if (top_keys[0].line <= (bottom_keys[bottom_keys.length - 1].line + 0.5)) {
        x_shift = top_note.getVoiceShiftWidth();
        bottom_note.setXShift(x_shift);
    }

    this.state.right_shift += x_shift;
    return this;
}

Vex.Flow.ModifierContext.prototype.formatNotesByY = function (notes) {
    // Called from formatNotes when more than two voices
    var hasStave = true;

    for (var i = 0; i < notes.length; i++) {
        hasStave = hasStave && notes[i].getStave() != null;
    }

    if (!hasStave) throw new Vex.RERR("Stave Missing",
      "All notes must have a stave - Vex.Flow.ModifierContext.formatMultiVoice!");

    var x_shift = 0;

    for (var i = 0; i < notes.length - 1; i++) {
        var top_note = notes[i];
        var bottom_note = notes[i + 1];

        if (top_note.getStemDirection() == Vex.Flow.StaveNote.STEM_DOWN) {
            top_note = notes[i + 1];
            bottom_note = notes[i];
        }

        var top_keys = top_note.getKeyProps();
        var bottom_keys = bottom_note.getKeyProps();

        var topY = top_note.getStave().getYForLine(top_keys[0].line);
        var bottomY = bottom_note.getStave().getYForLine(bottom_keys[bottom_keys.length - 1].line);

        var line_space = top_note.getStave().options.spacing_between_lines_px;
        if (Math.abs(topY - bottomY) == line_space / 2) {
            x_shift = top_note.getVoiceShiftWidth();
            bottom_note.setXShift(x_shift);
        }
    }

    this.state.right_shift += x_shift;
    return this;
}

Vex.Flow.ModifierContext.prototype.formatDots = function () {
    var right_shift = this.state.right_shift;
    var dots = this.modifiers['dots'];
    var dot_spacing = 1;

    if (!dots || dots.length == 0) return this;

    var dot_list = [];
    for (var i = 0; i < dots.length; ++i) {
        var dot = dots[i];
        var note = dot.getNote();
        var props = note.getKeyProps()[dot.getIndex()];
        var shift = (props.displaced ? note.getExtraRightPx() : 0);
        dot_list.push({ line: props.line, shift: shift, note: note, dot: dot });
    }

    // Sort dots by line number.
    dot_list.sort(function (a, b) { return (b.line - a.line); });

    var dot_shift = right_shift;
    var x_width = 0;
    var top_line = dot_list[0].line;
    var last_line = null;
    var last_note = null;
    for (var i = 0; i < dot_list.length; ++i) {
        var dot = dot_list[i].dot;
        var line = dot_list[i].line;
        var note = dot_list[i].note;
        var shift = dot_list[i].shift;

        // Reset the position of the dot every line.
        if (line != last_line || note != last_note) {
            dot_shift = right_shift + shift;
        }

        dot.setXShift(dot_shift);
        dot_shift += dot.getWidth() + dot_spacing; // spacing
        x_width = (dot_shift > x_width) ? dot_shift : x_width;
        last_line = line;
        last_note = note;
    }

    this.state.right_shift += x_width;
    return this;
}

Vex.Flow.ModifierContext.prototype.formatAccidentals = function () {
    var left_shift = this.state.left_shift;
    var accidentals = this.modifiers['accidentals'];
    var accidental_spacing = 2;

    if (!accidentals || accidentals.length == 0) return this;

    var acc_list = [];
    var hasStave = false;

    for (var i = 0; i < accidentals.length; ++i) {
        var acc = accidentals[i];
        var note = acc.getNote();
        var stave = note.getStave();
        var props = note.getKeyProps()[acc.getIndex()];
        var shift = (props.displaced ? note.getExtraLeftPx() : 0);
        if (stave != null) {
            hasStave = true;
            var line_space = stave.options.spacing_between_lines_px;
            var y = stave.getYForLine(props.line);
            acc_list.push({ y: y, shift: shift, acc: acc, lineSpace: line_space });
        } else {
            acc_list.push({ line: props.line, shift: shift, acc: acc });
        }
    }

    // If stave assigned, format based on note y-position
    if (hasStave)
        return this.formatAccidentalsByY(acc_list);

    // Sort accidentals by line number.
    acc_list.sort(function (a, b) { return (b.line - a.line); });

    // If first note left shift in case it is displaced
    var acc_shift = left_shift + acc_list[0].shift;
    var x_width = 0;
    var top_line = acc_list[0].line;
    for (var i = 0; i < acc_list.length; ++i) {
        var acc = acc_list[i].acc;
        var line = acc_list[i].line;
        var shift = acc_list[i].shift;

        // Once you hit three stave lines, you can reset the position of the
        // accidental.
        if (line < top_line - 3.0) {
            top_line = line;
            acc_shift = left_shift + shift;
        }

        acc.setXShift(acc_shift);
        acc_shift += acc.getWidth() + accidental_spacing; // spacing
        x_width = (acc_shift > x_width) ? acc_shift : x_width;
    }

    this.state.left_shift += x_width;
    return this;
}

Vex.Flow.ModifierContext.prototype.formatAccidentalsByY = function (acc_list) {
    var left_shift = this.state.left_shift;
    var accidental_spacing = 2;

    // Sort accidentals by Y-position.
    acc_list.sort(function (a, b) { return (b.y - a.y); });

    // If first note is displaced, get the correct left shift
    var acc_shift = left_shift + acc_list[0].shift;
    var x_width = 0;
    var top_y = acc_list[0].y;

    for (var i = 0; i < acc_list.length; ++i) {
        var acc = acc_list[i].acc;
        var y = acc_list[i].y;
        var shift = acc_list[i].shift;

        // Once you hit three stave lines, you can reset the position of the
        // accidental.
        if (top_y - y > 3 * acc_list[i].lineSpace) {
            top_y = y;
            acc_shift = left_shift + shift;
        }

        acc.setXShift(acc_shift);
        acc_shift += acc.getWidth() + accidental_spacing; // spacing
        x_width = (acc_shift > x_width) ? acc_shift : x_width;
    }

    this.state.left_shift += x_width;
    return this;
}

Vex.Flow.ModifierContext.prototype.formatStrokes = function () {
    var left_shift = this.state.left_shift;
    var strokes = this.modifiers['strokes'];
    var stroke_spacing = 0;

    if (!strokes || strokes.length == 0) return this;

    var str_list = [];
    for (var i = 0; i < strokes.length; ++i) {
        var str = strokes[i];
        var note = str.getNote();
        var props = note.getKeyProps()[str.getIndex()];
        var shift = (props.displaced ? note.getExtraLeftPx() : 0);
        str_list.push({ line: props.line, shift: shift, str: str });
    }

    var str_shift = left_shift;
    var x_shift = 0;

    // There can only be one stroke .. if more than one, they overlay each other
    for (var i = 0; i < str_list.length; ++i) {
        var str = str_list[i].str;
        var line = str_list[i].line;
        var shift = str_list[i].shift;
        str.setXShift(str_shift);
        x_shift = Math.max(str.getWidth() + stroke_spacing, x_shift);
    }

    this.state.left_shift += x_shift;
    return this;
}

Vex.Flow.ModifierContext.prototype.formatBends = function () {
    var right_shift = this.state.right_shift;
    var bends = this.modifiers['bends'];
    if (!bends || bends.length == 0) return this;

    var width = 0;
    var last_width = 0;
    var text_line = this.state.text_line;

    // Format Bends
    for (var i = 0; i < bends.length; ++i) {
        var bend = bends[i];
        bend.setXShift(last_width);
        last_width = bend.getWidth();
        bend.setTextLine(text_line);
    }

    this.state.right_shift += last_width;
    this.state.text_line += 1;
    return this;
}

Vex.Flow.ModifierContext.prototype.formatVibratos = function () {
    var vibratos = this.modifiers['vibratos'];
    if (!vibratos || vibratos.length == 0) return this;

    var text_line = this.state.text_line;
    var width = 0;
    var shift = this.state.right_shift - 7;

    // If there's a bend, drop the text line
    var bends = this.modifiers['bends'];
    if (bends && bends.length > 0) {
        text_line--;
    }

    // Format Vibratos
    for (var i = 0; i < vibratos.length; ++i) {
        var vibrato = vibratos[i];
        vibrato.setXShift(shift);
        vibrato.setTextLine(text_line);
        width += vibrato.getWidth();
        shift += width;
    }

    this.state.right_shift += width;
    this.state.text_line += 1;
    return this;
}

Vex.Flow.ModifierContext.prototype.formatAnnotations = function () {
    var annotations = this.modifiers['annotations'];
    if (!annotations || annotations.length == 0) return this;

    var text_line = this.state.text_line;
    var max_width = 0;

    // Format Annotations
    for (var i = 0; i < annotations.length; ++i) {
        var annotation = annotations[i];
        annotation.setTextLine(text_line);
        var width = annotation.getWidth() > max_width ?
          annotation.getWidth() : max_width;
        text_line++;
    }

    this.state.left_shift += width / 2;
    this.state.right_shift += width / 2;
    // No need to update text_line because we leave lots of room on the same
    // line.
    return this;
}

Vex.Flow.ModifierContext.prototype.formatArticulations = function () {
    var articulations = this.modifiers['articulations'];
    if (!articulations || articulations.length == 0) return this;

    var text_line = this.state.text_line;
    var max_width = 0;

    // Format Articulations
    for (var i = 0; i < articulations.length; ++i) {
        var articulation = articulations[i];
        articulation.setTextLine(text_line);
        var width = articulation.getWidth() > max_width ?
          articulation.getWidth() : max_width;
        text_line += 1.5;
    }

    this.state.left_shift += width / 2;
    this.state.right_shift += width / 2;
    this.state.text_line = text_line;
    return this;
}

Vex.Flow.ModifierContext.prototype.preFormat = function () {
    if (this.preFormatted) return;

    // Format modifiers in the following order:
    this.formatNotes().
         formatAccidentals().
         formatDots().
         formatStrokes().
         formatArticulations().
         formatAnnotations().
         formatBends().
         formatVibratos();

    // Update width of this modifier context
    this.width = this.state.left_shift + this.state.right_shift;
    this.preFormatted = true;
}